View Javadoc

1   /**
2    * Copyright 2008 WebPhotos
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package net.sf.webphotos.gui.util;
17  
18  import java.awt.*;
19  import java.awt.event.*;
20  import java.util.*;
21  import java.util.List;
22  import javax.swing.*;
23  import javax.swing.event.TableModelEvent;
24  import javax.swing.event.TableModelListener;
25  import javax.swing.table.*;
26  
27  /**
28   * TableSorter is a decorator for TableModels; adding sorting
29   * functionality to a supplied TableModel. TableSorter does
30   * not store or copy the data in its TableModel; instead it maintains
31   * a map from the row indexes of the view to the row indexes of the
32   * model. As requests are made of the sorter (like getValueAt(row, col))
33   * they are passed to the underlying model after the row numbers
34   * have been translated via the internal mapping array. This way,
35   * the TableSorter appears to hold another copy of the table
36   * with the rows in a different order.
37   * <p/>
38   * TableSorter registers itself as a listener to the underlying model,
39   * just as the JTable itself would. Events recieved from the model
40   * are examined, sometimes manipulated (typically widened), and then
41   * passed on to the TableSorter's listeners (typically the JTable).
42   * If a change to the model has invalidated the order of TableSorter's
43   * rows, a note of this is made and the sorter will resort the
44   * rows the next time a value is requested.
45   * <p/>
46   * When the tableHeader property is set, either by using the
47   * setTableHeader() method or the two argument constructor, the
48   * table header may be used as a complete UI for TableSorter.
49   * The default renderer of the tableHeader is decorated with a renderer
50   * that indicates the sorting status of each column. In addition,
51   * a mouse listener is installed with the following behavior:
52   * <ul>
53   * <li>
54   * Mouse-click: Clears the sorting status of all other columns
55   * and advances the sorting status of that column through three
56   * values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to
57   * NOT_SORTED again).
58   * <li>
59   * SHIFT-mouse-click: Clears the sorting status of all other columns
60   * and cycles the sorting status of the column through the same
61   * three values, in the opposite order: {NOT_SORTED, DESCENDING, ASCENDING}.
62   * <li>
63   * CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except
64   * that the changes to the column do not cancel the statuses of columns
65   * that are already sorting - giving a way to initiate a compound
66   * sort.
67   * </ul>
68   * <p/>
69   * This is a long overdue rewrite of a class of the same name that
70   * first appeared in the swing table demos in 1997.
71   * 
72   * @author Philip Milne
73   * @author Brendon McLean 
74   * @author Dan van Enckevort
75   * @author Parwinder Sekhon
76   * @version 2.0 02/27/04
77   */
78  public final class TableSorter extends AbstractTableModel {
79  
80      private static final long serialVersionUID = -5572044930561418466L;
81      /**
82       *
83       */
84      protected TableModel tableModel;
85      /**
86       *
87       */
88      public static final int DESCENDING = -1;
89      /**
90       *
91       */
92      public static final int NOT_SORTED = 0;
93      /**
94       *
95       */
96      public static final int ASCENDING = 1;
97      private static Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED);
98      /**
99       *
100      */
101     public static final Comparator<Comparable<Object>> COMPARABLE_COMPARATOR = new Comparator<Comparable<Object>>() {
102 
103         @Override
104         public int compare(Comparable<Object> o1, Comparable<Object> o2) {
105             return o1.compareTo(o2);
106         }
107     };
108     /**
109      *
110      */
111     public static final Comparator<Comparable<Object>> LEXICAL_COMPARATOR = new Comparator<Comparable<Object>>() {
112 
113         @Override
114         public int compare(Comparable<Object> o1, Comparable<Object> o2) {
115             return o1.toString().compareTo(o2.toString());
116         }
117     };
118     private Row[] viewToModel;
119     private int[] modelToView;
120     private JTableHeader tableHeader;
121     private MouseListener mouseListener;
122     private TableModelListener tableModelListener;
123     private Map<Class<?>, Comparator<?>> columnComparators = new HashMap<Class<?>, Comparator<?>>();
124     private List<Directive> sortingColumns = new ArrayList<Directive>();
125 
126     /**
127      *
128      */
129     public TableSorter() {
130         this.mouseListener = new MouseHandler();
131         this.tableModelListener = new TableModelHandler();
132     }
133 
134     /**
135      *
136      * @param tableModel
137      */
138     public TableSorter(TableModel tableModel) {
139         this();
140         setTableModel(tableModel);
141     }
142 
143     /**
144      *
145      * @param tableModel
146      * @param tableHeader
147      */
148     public TableSorter(TableModel tableModel, JTableHeader tableHeader) {
149         this();
150         setTableHeader(tableHeader);
151         setTableModel(tableModel);
152     }
153 
154     private void clearSortingState() {
155         viewToModel = null;
156         modelToView = null;
157     }
158 
159     /**
160      *
161      * @return
162      */
163     public TableModel getTableModel() {
164         return tableModel;
165     }
166 
167     /**
168      *
169      * @param tableModel
170      */
171     public void setTableModel(TableModel tableModel) {
172         if (this.tableModel != null) {
173             this.tableModel.removeTableModelListener(tableModelListener);
174         }
175 
176         this.tableModel = tableModel;
177         if (this.tableModel != null) {
178             this.tableModel.addTableModelListener(tableModelListener);
179         }
180 
181         clearSortingState();
182         fireTableStructureChanged();
183     }
184 
185     /**
186      *
187      * @return
188      */
189     public JTableHeader getTableHeader() {
190         return tableHeader;
191     }
192 
193     /**
194      *
195      * @param tableHeader
196      */
197     public void setTableHeader(JTableHeader tableHeader) {
198         if (this.tableHeader != null) {
199             this.tableHeader.removeMouseListener(mouseListener);
200             TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer();
201             if (defaultRenderer instanceof SortableHeaderRenderer) {
202                 this.tableHeader.setDefaultRenderer(((SortableHeaderRenderer) defaultRenderer).tableCellRenderer);
203             }
204         }
205         this.tableHeader = tableHeader;
206         if (this.tableHeader != null) {
207             this.tableHeader.addMouseListener(mouseListener);
208             this.tableHeader.setDefaultRenderer(
209                     new SortableHeaderRenderer(this.tableHeader.getDefaultRenderer()));
210         }
211     }
212 
213     /**
214      *
215      * @return
216      */
217     public boolean isSorting() {
218         return !sortingColumns.isEmpty();
219     }
220 
221     private Directive getDirective(int column) {
222         for (int i = 0; i < sortingColumns.size(); i++) {
223             Directive directive = (Directive) sortingColumns.get(i);
224             if (directive.column == column) {
225                 return directive;
226             }
227         }
228         return EMPTY_DIRECTIVE;
229     }
230 
231     /**
232      *
233      * @param column
234      * @return
235      */
236     public int getSortingStatus(int column) {
237         return getDirective(column).direction;
238     }
239 
240     private void sortingStatusChanged() {
241         clearSortingState();
242         fireTableDataChanged();
243         if (tableHeader != null) {
244             tableHeader.repaint();
245         }
246     }
247 
248     /**
249      *
250      * @param column
251      * @param status
252      */
253     public void setSortingStatus(int column, int status) {
254         Directive directive = getDirective(column);
255         if (directive != EMPTY_DIRECTIVE) {
256             sortingColumns.remove(directive);
257         }
258         if (status != NOT_SORTED) {
259             sortingColumns.add(new Directive(column, status));
260         }
261         sortingStatusChanged();
262     }
263 
264     /**
265      *
266      * @param column
267      * @param size
268      * @return
269      */
270     protected Icon getHeaderRendererIcon(int column, int size) {
271         Directive directive = getDirective(column);
272         if (directive == EMPTY_DIRECTIVE) {
273             return null;
274         }
275         return new Arrow(directive.direction == DESCENDING, size, sortingColumns.indexOf(directive));
276     }
277 
278     private void cancelSorting() {
279         sortingColumns.clear();
280         sortingStatusChanged();
281     }
282 
283     /**
284      *
285      * @param type
286      * @param comparator
287      */
288     public void setColumnComparator(Class<?> type, Comparator<?> comparator) {
289         if (comparator == null) {
290             columnComparators.remove(type);
291         } else {
292             columnComparators.put(type, comparator);
293         }
294     }
295 
296     /**
297      *
298      * @param column
299      * @return
300      */
301     @SuppressWarnings("unchecked")
302     protected Comparator<Comparable<Object>> getComparator(int column) {
303         Class<?> columnType = tableModel.getColumnClass(column);
304         Comparator<Comparable<Object>> comparator = (Comparator<Comparable<Object>>) columnComparators.get(columnType);
305         if (comparator != null) {
306             return comparator;
307         }
308         if (Comparable.class.isAssignableFrom(columnType)) {
309             return COMPARABLE_COMPARATOR;
310         }
311         return LEXICAL_COMPARATOR;
312     }
313 
314     private Row[] getViewToModel() {
315         if (viewToModel == null) {
316             int tableModelRowCount = tableModel.getRowCount();
317             viewToModel = new Row[tableModelRowCount];
318             for (int row = 0; row < tableModelRowCount; row++) {
319                 viewToModel[row] = new Row(row);
320             }
321 
322             if (isSorting()) {
323                 Arrays.sort(viewToModel);
324             }
325         }
326         return viewToModel;
327     }
328 
329     /**
330      *
331      * @param viewIndex
332      * @return
333      */
334     public int modelIndex(int viewIndex) {
335         return getViewToModel()[viewIndex].modelIndex;
336     }
337 
338     private int[] getModelToView() {
339         if (modelToView == null) {
340             int n = getViewToModel().length;
341             modelToView = new int[n];
342             for (int i = 0; i < n; i++) {
343                 modelToView[modelIndex(i)] = i;
344             }
345         }
346         return modelToView;
347     }
348 
349     // TableModel interface methods 
350     @Override
351     public int getRowCount() {
352         return (tableModel == null) ? 0 : tableModel.getRowCount();
353     }
354 
355     @Override
356     public int getColumnCount() {
357         return (tableModel == null) ? 0 : tableModel.getColumnCount();
358     }
359 
360     @Override
361     public String getColumnName(int column) {
362         return tableModel.getColumnName(column);
363     }
364 
365     @Override
366     public Class<?> getColumnClass(int column) {
367         return tableModel.getColumnClass(column);
368     }
369 
370     @Override
371     public boolean isCellEditable(int row, int column) {
372         return tableModel.isCellEditable(modelIndex(row), column);
373     }
374 
375     @Override
376     public Object getValueAt(int row, int column) {
377         return tableModel.getValueAt(modelIndex(row), column);
378     }
379 
380     @Override
381     public void setValueAt(Object aValue, int row, int column) {
382         tableModel.setValueAt(aValue, modelIndex(row), column);
383     }
384 
385     // Helper classes
386     private class Row implements Comparable<Row> {
387 
388         private int modelIndex;
389 
390         public Row(int index) {
391             this.modelIndex = index;
392         }
393 
394         @SuppressWarnings("unchecked")
395         @Override
396         public int compareTo(Row o) {
397             int row1 = modelIndex;
398             int row2 = o.modelIndex;
399 
400             for (Iterator<Directive> it = sortingColumns.iterator(); it.hasNext();) {
401                 Directive directive = (Directive) it.next();
402                 int column = directive.column;
403                 Object o1 = tableModel.getValueAt(row1, column);
404                 Object o2 = tableModel.getValueAt(row2, column);
405 
406                 int comparison = 0;
407                 // Define null less than everything, except null.
408                 if (o1 == null && o2 == null) {
409                     comparison = 0;
410                 } else if (o1 == null) {
411                     comparison = -1;
412                 } else if (o2 == null) {
413                     comparison = 1;
414                 } else {
415                     comparison = getComparator(column).compare((Comparable<Object>) o1, (Comparable<Object>) o2);
416                 }
417                 if (comparison != 0) {
418                     return directive.direction == DESCENDING ? -comparison : comparison;
419                 }
420             }
421             return 0;
422         }
423     }
424 
425     private class TableModelHandler implements TableModelListener {
426 
427         @Override
428         public void tableChanged(TableModelEvent e) {
429             // If we're not sorting by anything, just pass the event along.             
430             if (!isSorting()) {
431                 clearSortingState();
432                 fireTableChanged(e);
433                 return;
434             }
435 
436             // If the table structure has changed, cancel the sorting; the             
437             // sorting columns may have been either moved or deleted from             
438             // the model. 
439             if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
440                 cancelSorting();
441                 fireTableChanged(e);
442                 return;
443             }
444 
445             // We can map a cell event through to the view without widening             
446             // when the following conditions apply: 
447             // 
448             // a) all the changes are on one row (e.getFirstRow() == e.getLastRow()) and, 
449             // b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and,
450             // c) we are not sorting on that column (getSortingStatus(column) == NOT_SORTED) and, 
451             // d) a reverse lookup will not trigger a sort (modelToView != null)
452             //
453             // Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS.
454             // 
455             // The last check, for (modelToView != null) is to see if modelToView 
456             // is already allocated. If we don't do this check; sorting can become 
457             // a performance bottleneck for applications where cells  
458             // change rapidly in different parts of the table. If cells 
459             // change alternately in the sorting column and then outside of             
460             // it this class can end up re-sorting on alternate cell updates - 
461             // which can be a performance problem for large tables. The last 
462             // clause avoids this problem. 
463             int column = e.getColumn();
464             if (e.getFirstRow() == e.getLastRow()
465                     && column != TableModelEvent.ALL_COLUMNS
466                     && getSortingStatus(column) == NOT_SORTED
467                     && modelToView != null) {
468                 int viewIndex = getModelToView()[e.getFirstRow()];
469                 fireTableChanged(new TableModelEvent(TableSorter.this,
470                         viewIndex, viewIndex,
471                         column, e.getType()));
472                 return;
473             }
474 
475             // Something has happened to the data that may have invalidated the row order. 
476             clearSortingState();
477             fireTableDataChanged();
478         }
479     }
480 
481     private class MouseHandler extends MouseAdapter {
482 
483         @Override
484         public void mouseClicked(MouseEvent e) {
485             JTableHeader h = (JTableHeader) e.getSource();
486             TableColumnModel columnModel = h.getColumnModel();
487             int viewColumn = columnModel.getColumnIndexAtX(e.getX());
488             int column = columnModel.getColumn(viewColumn).getModelIndex();
489             if (column != -1) {
490                 int status = getSortingStatus(column);
491                 if (!e.isControlDown()) {
492                     cancelSorting();
493                 }
494                 // Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or 
495                 // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed. 
496                 status = status + (e.isShiftDown() ? -1 : 1);
497                 status = (status + 4) % 3 - 1; // signed mod, returning {-1, 0, 1}
498                 setSortingStatus(column, status);
499             }
500         }
501     }
502 
503     private static class Arrow implements Icon {
504 
505         private boolean descending;
506         private int size;
507         private int priority;
508 
509         public Arrow(boolean descending, int size, int priority) {
510             this.descending = descending;
511             this.size = size;
512             this.priority = priority;
513         }
514 
515         @Override
516         public void paintIcon(Component c, Graphics g, int x, int y) {
517             Color color = c == null ? Color.GRAY : c.getBackground();
518             // In a compound sort, make each succesive triangle 20% 
519             // smaller than the previous one. 
520             int dx = (int) (size / 2 * Math.pow(0.8, priority));
521             int dy = descending ? dx : -dx;
522             // Align icon (roughly) with font baseline. 
523             y = y + 5 * size / 6 + (descending ? -dy : 0);
524             int shift = descending ? 1 : -1;
525             g.translate(x, y);
526 
527             // Right diagonal. 
528             g.setColor(color.darker());
529             g.drawLine(dx / 2, dy, 0, 0);
530             g.drawLine(dx / 2, dy + shift, 0, shift);
531 
532             // Left diagonal. 
533             g.setColor(color.brighter());
534             g.drawLine(dx / 2, dy, dx, 0);
535             g.drawLine(dx / 2, dy + shift, dx, shift);
536 
537             // Horizontal line. 
538             if (descending) {
539                 g.setColor(color.darker().darker());
540             } else {
541                 g.setColor(color.brighter().brighter());
542             }
543             g.drawLine(dx, 0, 0, 0);
544 
545             g.setColor(color);
546             g.translate(-x, -y);
547         }
548 
549         @Override
550         public int getIconWidth() {
551             return size;
552         }
553 
554         @Override
555         public int getIconHeight() {
556             return size;
557         }
558     }
559 
560     private class SortableHeaderRenderer implements TableCellRenderer {
561 
562         private TableCellRenderer tableCellRenderer;
563 
564         public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) {
565             this.tableCellRenderer = tableCellRenderer;
566         }
567 
568         @Override
569         public Component getTableCellRendererComponent(JTable table,
570                 Object value,
571                 boolean isSelected,
572                 boolean hasFocus,
573                 int row,
574                 int column) {
575             Component c = tableCellRenderer.getTableCellRendererComponent(table,
576                     value, isSelected, hasFocus, row, column);
577             if (c instanceof JLabel) {
578                 JLabel l = (JLabel) c;
579                 l.setHorizontalTextPosition(JLabel.LEFT);
580                 int modelColumn = table.convertColumnIndexToModel(column);
581                 l.setIcon(getHeaderRendererIcon(modelColumn, l.getFont().getSize()));
582             }
583             return c;
584         }
585     }
586 
587     private static class Directive {
588 
589         private int column;
590         private int direction;
591 
592         public Directive(int column, int direction) {
593             this.column = column;
594             this.direction = direction;
595         }
596     }
597 }